Rate limit and brute force protection. Some updates to landing page a…#103
Conversation
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 36 minutes and 31 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (14)
📝 WalkthroughWalkthroughThe pull request adds Spring Boot Actuator dependency and implements comprehensive authentication and rate-limiting security infrastructure, including rate-limit filters with configurable policies, login brute-force prevention with account lockout, custom authentication handlers, login attempt tracking, Micrometer observability metrics, and updates the landing page UI to replace emoji with slide-based navigation. Changes
Sequence DiagramssequenceDiagram
participant Client
participant RateLimitFilter
participant RateLimitService
participant LoginLockoutFilter
participant LoginAttemptService
participant AuthenticationManager
participant SuccessHandler
participant SecurityObservabilityService
Client->>RateLimitFilter: POST /login (username, password)
RateLimitFilter->>RateLimitService: policyForPath("/login", "POST")
RateLimitService-->>RateLimitFilter: Optional<RateLimitPolicy>
alt Policy matches
RateLimitFilter->>RateLimitService: evaluate(policy, clientIp)
RateLimitService-->>RateLimitFilter: RateLimitDecision
alt Denied (too many requests)
RateLimitFilter->>SecurityObservabilityService: recordRateLimitDenied(policy)
RateLimitFilter-->>Client: HTTP 429 + Retry-After header
else Allowed
RateLimitFilter->>LoginLockoutFilter: filterChain.doFilter()
end
else No policy
RateLimitFilter->>LoginLockoutFilter: filterChain.doFilter()
end
LoginLockoutFilter->>LoginAttemptService: currentLockDecision(username, clientIp)
LoginAttemptService-->>LoginLockoutFilter: LockDecision
alt Account/IP is locked
LoginLockoutFilter->>SecurityObservabilityService: recordLoginLocked()
LoginLockoutFilter-->>Client: Redirect /login?error=true&locked=true&retryAfter=N
else Not locked
LoginLockoutFilter->>AuthenticationManager: authenticate(credentials)
alt Authentication succeeds
AuthenticationManager-->>SuccessHandler: onAuthenticationSuccess()
SuccessHandler->>LoginAttemptService: recordSuccess(username, clientIp)
LoginAttemptService->>SecurityObservabilityService: recordLoginSuccessReset()
SuccessHandler-->>Client: Redirect /home
else Authentication fails
AuthenticationManager-->>Client: AuthenticationException
Client->>LoginAttemptService: recordFailure(username, clientIp)
LoginAttemptService->>SecurityObservabilityService: recordLoginFailure()
alt Failures exceed threshold
LoginAttemptService->>SecurityObservabilityService: recordLoginLocked()
LoginAttemptService-->>Client: LockDecision(locked=true, retryAfterSeconds=900)
else Below threshold
LoginAttemptService-->>Client: LockDecision(locked=false, retryAfterSeconds=0)
end
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (9)
src/main/resources/templates/login/login.html (1)
24-26: Optional: surfaceretryAfterin the lockout message.
LoginLockoutFilteralready redirects withretryAfter=<seconds>(per the filter test). Consider showing the remaining time so users understand how long to wait, e.g.:<div th:if="${param.locked}" class="panel panel-error" style="margin-bottom: 14px;"> Too many login attempts. Please try again in <span th:text="${param.retryAfter != null ? param.retryAfter + ' seconds' : 'a few minutes'}">a few minutes</span>. </div>Functional as-is; this is purely a UX polish.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/resources/templates/login/login.html` around lines 24 - 26, Update the login template to display the lockout remaining time provided by LoginLockoutFilter by reading the retryAfter request param instead of the static phrase; inside the existing th:if="${param.locked}" block use param.retryAfter (e.g. show "${param.retryAfter} seconds" when present, fallback to the current "a few minutes" text) so users see the actual wait time returned by LoginLockoutFilter.src/test/java/org/example/projektarendehantering/infrastructure/security/LoginLockoutFilterTest.java (1)
12-50: Consider additional coverage for non-matching requests.Current tests cover only
POST /loginpaths. To harden the filter's request matching, consider adding cases that exercise:
GET /login(should bypass the lock check),- requests to other paths (e.g.
/register,/api/…) that should not be intercepted,- missing
usernameparameter (verify no NPE and expected behavior).Not a blocker — the happy paths are well-tested.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/test/java/org/example/projektarendehantering/infrastructure/security/LoginLockoutFilterTest.java` around lines 12 - 50, Add tests to LoginLockoutFilterTest that exercise non-matching requests: create cases invoking LoginLockoutFilter.doFilter with (1) a GET "/login" request (MockHttpServletRequest with method "GET") and assert the filter does not call redirect and the chain proceeded, (2) a POST to a different path like "/register" or "/api/foo" and assert the same, and (3) a POST "/login" with no "username" parameter to verify no NPE and that the request is passed through; in each test mock LoginAttemptService.currentLockDecision only if expected to be called (and verify it is not called for non-matching requests) and use MockFilterChain/MockHttpServletResponse assertions (response.getRedirectedUrl() == null, response.getStatus()==200 or chain proceeded) to confirm correct behavior.src/main/java/org/example/projektarendehantering/infrastructure/security/LoginLockoutFilter.java (1)
47-48: Nit:URLEncoder.encodeon a numericretryAfteris unnecessary.
decision.retryAfterSeconds()is along/int; its decimal representation is URL-safe. The encoding adds no protection and marginally obscures the value in logs. Inliningdecision.retryAfterSeconds()directly is cleaner (same applies inLoginAuthenticationFailureHandlerLine 38).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/org/example/projektarendehantering/infrastructure/security/LoginLockoutFilter.java` around lines 47 - 48, The redirect currently URL-encodes a numeric retryAfter value unnecessarily; in LoginLockoutFilter replace the URLEncoder.encode usage and append decision.retryAfterSeconds() directly (or its String form) when building the "/login?error=true&locked=true&retryAfter=" redirect, and make the same change in LoginAuthenticationFailureHandler at the analogous location (line ~38) so the numeric seconds are inlined without encoding.src/test/java/org/example/projektarendehantering/infrastructure/security/RateLimitServiceTest.java (1)
58-79: Optional: extractMutableClockinto a shared test helper.The same private
MutableClockis reintroduced verbatim inLoginAttemptServiceTest. Consider moving it to a package-private test utility (e.g.,infrastructure/security/testing/MutableClock) to avoid drift if the helper evolves.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/test/java/org/example/projektarendehantering/infrastructure/security/RateLimitServiceTest.java` around lines 58 - 79, Extract the private MutableClock inner class used in RateLimitServiceTest into a shared package-private test utility so LoginAttemptServiceTest can reuse it; specifically, create a new test helper class named MutableClock (matching the current constructor and overrides: instant(), getZone(), withZone()) in a shared test package (e.g., infrastructure.security.testing) and replace the private inner MutableClock in both RateLimitServiceTest and LoginAttemptServiceTest with imports of that shared MutableClock. Ensure the new helper preserves the same API (constructor taking Instant and the three overridden methods) and visibility so both tests can access it.src/test/java/org/example/projektarendehantering/infrastructure/security/LoginAuthenticationHandlersTest.java (1)
32-47: Optional: add a case that exercises a non-canonical username.Both interactions use
"user@example.com"verbatim, so the test cannot detect inconsistent normalization betweenrecordFailure(request.getParameter("username"), ...)andrecordSuccess(authentication.getName(), ...)(see the verification comment onLoginAuthenticationSuccessHandler). Consider a case where the form field is" User@Example.com "while the authenticated principal is"user@example.com"to assert the service key is consistent.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/test/java/org/example/projektarendehantering/infrastructure/security/LoginAuthenticationHandlersTest.java` around lines 32 - 47, Add a new test that verifies normalization consistency between the submitted username and the authentication principal: create a LoginAuthenticationSuccessHandler with a mocked LoginAttemptService, simulate a request whose form username parameter is a non-canonical value (e.g., " User@Example.com "), set request remoteAddr, build an Authentication whose getName() returns the canonical "user@example.com", call handler.onAuthenticationSuccess(request, response, authentication), and verify LoginAttemptService.recordSuccess was called with the canonical username ("user@example.com") and the request IP; ensure the response redirect assertion ("/home") remains.src/test/java/org/example/projektarendehantering/infrastructure/security/LoginAttemptServiceTest.java (1)
13-51: LGTM — consider optional coverage for per-IP vs per-user keying.Solid tests for threshold, TTL expiry, and success-clears behaviour. If the service keys lockouts per
(username, ip)pair (or per either), add a test asserting that failures from a different IP don't lock the same user, and/or that failures against different usernames from the same IP behave as expected. This tends to be the surface where brute-force bypasses creep in.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/test/java/org/example/projektarendehantering/infrastructure/security/LoginAttemptServiceTest.java` around lines 13 - 51, Add tests to verify whether LoginAttemptService keys by (username, ip) pair or by single dimension: create new test(s) that use LoginAttemptService (constructor used in existing tests) and call recordFailure for "user@example.com" from "127.0.0.1" enough times to hit the threshold, then assert currentLockDecision("user@example.com","127.0.0.2").locked() is false (different IP) and/or assert currentLockDecision("other@example.com","127.0.0.1").locked() is false (different user); also add the inverse case if needed to document behavior. Target the same class and methods: LoginAttemptService.recordFailure, LoginAttemptService.currentLockDecision, and LoginAttemptService.recordSuccess to show expected per-IP vs per-user semantics.src/main/resources/static/app.js (2)
110-133: Consider throttlingsyncActiveSlideFromViewport.The listener runs on every scroll event — including the scroll events emitted during a programmatic
scrollIntoViewfromgoToSlide— and each call doesslides.length×getBoundingClientRect()(which forces a layout). For a handful of slides it's fine, butrequestAnimationFrame-coalescing (or a simple 16ms throttle) keeps this off the main thread during inertial scrolling and avoids the brief flicker of active-link state during smooth-scroll animations.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/resources/static/app.js` around lines 110 - 133, Thottle scroll handler by coalescing syncActiveSlideFromViewport calls with requestAnimationFrame: add a module-level flag (e.g., isTicking) and in the scroll listener (the window.addEventListener("scroll", ...)) only schedule syncActiveSlideFromViewport via requestAnimationFrame when not already scheduled, clearing the flag inside syncActiveSlideFromViewport after work completes; ensure the listener remains passive and keep references to slides/getBoundingClientRect logic as-is so goToSlide-triggered programmatic scrolls are coalesced and avoid repeated forced layouts.
302-310: Simplify initial hash handling.The first
goToSlide(..., { shouldScroll: false })is used only to sync visual state, then a second call withshouldScroll: trueperforms the actual scroll. SincegoToSlidealready runssyncBySlide()before scrolling, a single call covers both paths:♻️ Proposed simplification
- const hash = window.location.hash.replace("#", ""); - const initialIndex = hash ? slideIds.indexOf(hash) : 0; - activeSlideIndex = initialIndex >= 0 ? initialIndex : 0; - goToSlide(activeSlideIndex, { behavior: "auto", shouldScroll: false, updateHash: false }); - if (hash) { - goToSlide(activeSlideIndex, { behavior: "auto", shouldScroll: true, updateHash: false }); - } else { - updateProgress(); - } + const hash = window.location.hash.replace("#", ""); + const initialIndex = hash ? slideIds.indexOf(hash) : 0; + const startIndex = initialIndex >= 0 ? initialIndex : 0; + goToSlide(startIndex, { behavior: "auto", shouldScroll: Boolean(hash), updateHash: false });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/resources/static/app.js` around lines 302 - 310, Replace the two goToSlide calls and the if/else block with a single call: determine hash and initialIndex as before, set activeSlideIndex, then call goToSlide(activeSlideIndex, { behavior: "auto", shouldScroll: !!hash, updateHash: false }); and remove the redundant first call and the separate conditional branch (including the alternate updateProgress() call) since goToSlide runs syncBySlide() and will handle progress/scrolling appropriately.src/main/java/org/example/projektarendehantering/infrastructure/config/SecurityConfig.java (1)
69-70: Chain the second filter relative to the first to make the execution order explicit.Both filters are added before
UsernamePasswordAuthenticationFilter. While modern Spring Security (6.x+) handles filter ordering correctly, relying on insertion order for implicit sequencing is unclear. Spring Security's documentation recommends chaining filter positions explicitly when order matters.Make the intended
RateLimitFilter → LoginLockoutFilter → UsernamePasswordAuthenticationFilterpipeline unambiguous:Suggested refactor
- .addFilterBefore(rateLimitFilter, UsernamePasswordAuthenticationFilter.class) - .addFilterBefore(loginLockoutFilter, UsernamePasswordAuthenticationFilter.class); + .addFilterBefore(rateLimitFilter, UsernamePasswordAuthenticationFilter.class) + .addFilterAfter(loginLockoutFilter, RateLimitFilter.class);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/org/example/projektarendehantering/infrastructure/config/SecurityConfig.java` around lines 69 - 70, The two filters (rateLimitFilter and loginLockoutFilter) are both added before UsernamePasswordAuthenticationFilter which leaves their relative order implicit; update SecurityConfig so the rate limit filter is explicitly placed before the login lockout filter and the login lockout filter is placed before UsernamePasswordAuthenticationFilter to enforce RateLimitFilter → LoginLockoutFilter → UsernamePasswordAuthenticationFilter. Concretely, call addFilterBefore(rateLimitFilter, LoginLockoutFilter.class) and then addFilterBefore(loginLockoutFilter, UsernamePasswordAuthenticationFilter.class) (using the actual filter implementation classes for LoginLockoutFilter) in the SecurityConfig configuration where the filters are registered.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@src/main/java/org/example/projektarendehantering/infrastructure/security/LoginAttemptService.java`:
- Around line 27-28: The emailAttempts and ipAttempts ConcurrentHashMaps in
LoginAttemptService grow unbounded (entries only added in registerFailure and
removed on success); add time-based eviction: either replace
emailAttempts/ipAttempts with Caffeine caches configured with
expireAfterAccess(failureWindowSeconds + lockDurationSeconds, SECONDS) and a
reasonable maximumSize, or implement a lightweight scheduled cleanup in
LoginAttemptService constructor that periodically iterates emailAttempts and
ipAttempts and removes entries where AttemptState.lockedUntilEpochSecond <= now
AND (now - AttemptState.windowStartEpochSecond) >= failureWindowSeconds;
reference the fields emailAttempts, ipAttempts, the registerFailure method,
AttemptState, and the configuration values
failureWindowSeconds/lockDurationSeconds when adding the eviction logic and
scheduling the task.
- Around line 75-78: The log lines in LoginAttemptService are writing raw,
attacker-controlled PII (email and clientIp) and allow log-forgery because
normalizeEmail only trims/lowers; fix by introducing a logging-only sanitizer
and pseudonymizer (e.g., a helper method like pseudonymizeForLogs(String value,
String appSalt) that first removes CR/LF and other control chars then computes a
truncated SHA-256 hash or returns only the email domain for emails and a
hashed/truncated IP), keep using normalizeEmail/normalizeIp for internal keys
and rate-limits but replace the values passed to log.info/log.warn in the
methods that emit
"security_event=LOGIN_FAILED"/"security_event=LOGIN_LOCKED"/etc. with the
sanitized/pseudonymized outputs from that helper, and ensure the helper is
reused across all three log sites to avoid duplication.
- Around line 83-89: recordSuccess currently removes both emailAttempts and
ipAttempts which resets IP-level counters and undermines per-IP lockouts; change
recordSuccess(String email, String clientIp) to only remove
normalizeEmail(email) from emailAttempts and do not remove normalizeIp(clientIp)
from ipAttempts (or if you prefer a conservative approach, only remove the IP
entry when ipAttempts.get(normalizeIp(clientIp)).failureCount < THRESHOLD), then
keep the calls to securityObservabilityService.recordLoginSuccessReset() and
setActiveLoginLocks(nowEpochSecond()) and the log using
normalizeEmail/normalizeIp; this preserves per-IP protections while still
clearing per-email state.
In
`@src/main/java/org/example/projektarendehantering/infrastructure/security/LoginLockoutFilter.java`:
- Line 38: LoginLockoutFilter currently uses request.getRemoteAddr() which
yields the proxy IP; update the code to extract the client IP from the
X-Forwarded-For header (parse the first non-empty entry) with fallback to
request.getRemoteAddr(), matching the logic used in AuditInterceptor. Replace
the single-line assignment of clientIp in LoginLockoutFilter and apply the same
extraction strategy in LoginAuthenticationFailureHandler (line ~33),
LoginAuthenticationSuccessHandler (line ~28) and RateLimitFilter (line ~49),
ensuring each place uses the X-Forwarded-For first value if present, otherwise
request.getRemoteAddr(), and keep the extraction logic consistent by reusing a
small helper method or inline the same parsing routine.
In
`@src/main/java/org/example/projektarendehantering/infrastructure/security/RateLimitFilter.java`:
- Line 49: The rate limiter is using request.getRemoteAddr() (in RateLimitFilter
and LoginLockoutFilter) which breaks behind proxies; update the app to obtain
the real client IP either by enabling Spring/Tomcat forwarded header processing
(e.g., set server.forward-headers-strategy=native and configure trusted
proxies/internal-proxies so RemoteIpValve parses X-Forwarded-For) or add a
forwarded-aware resolver (e.g., a helper used by RateLimitFilter and
LoginLockoutFilter that extracts the left-most trusted entry from
X-Forwarded-For with validation and falls back to getRemoteAddr()), and add a
short comment documenting the trusted-proxy model so headers aren’t blindly
trusted.
In
`@src/main/java/org/example/projektarendehantering/infrastructure/security/RateLimitService.java`:
- Line 25: The counters map (field counters in RateLimitService) is unbounded
and can leak memory; replace the ConcurrentMap<String, CounterWindow> with a
Caffeine Cache<String, CounterWindow> configured with expireAfterAccess or
expireAfterWrite (set to at least the largest policy window) and a sensible
maximumSize to evict old client entries, then update usages (where CounterWindow
instances are retrieved/created — e.g., the code paths that currently call
counters.get or put, and the logic around creating new
CounterWindow(nowEpochSecond)) to use cache.get(key, mappingFunction) so stale
entries are auto-evicted and the limiter cannot grow unbounded.
In `@src/main/resources/application.properties`:
- Around line 52-53: The properties enable Prometheus via
management.endpoints.web.exposure.include=health,info,metrics,prometheus but the
Micrometer Prometheus registry dependency is missing; either add the
io.micrometer:micrometer-registry-prometheus dependency to your build (so the
prometheus actuator endpoint is registered) or remove "prometheus" from the
exposure list in application.properties; note actuator endpoints are already
protected by the catch-all .anyRequest().authenticated() rule in SecurityConfig,
so no additional authorization rules are required.
In `@src/main/resources/static/app.css`:
- Around line 717-726: Keyframe names panelGlow and shimmerSweep violate
kebab-case; rename them to panel-glow and shimmer-sweep in the `@keyframes` blocks
and update every CSS usage that references them (e.g., any animation: or
animation-name: properties that currently use panelGlow or shimmerSweep) to use
the new kebab-case identifiers so stylelint passes.
In `@src/main/resources/static/app.js`:
- Around line 134-136: The resize handler currently calls syncBySlide(), which
only redraws using the existing activeSlideIndex and can leave the nav
out-of-sync; replace the call to syncBySlide() with
syncActiveSlideFromViewport() so the handler re-derives the active slide from
the viewport (syncActiveSlideFromViewport already falls back to updateProgress()
when the index doesn't change).
---
Nitpick comments:
In
`@src/main/java/org/example/projektarendehantering/infrastructure/config/SecurityConfig.java`:
- Around line 69-70: The two filters (rateLimitFilter and loginLockoutFilter)
are both added before UsernamePasswordAuthenticationFilter which leaves their
relative order implicit; update SecurityConfig so the rate limit filter is
explicitly placed before the login lockout filter and the login lockout filter
is placed before UsernamePasswordAuthenticationFilter to enforce RateLimitFilter
→ LoginLockoutFilter → UsernamePasswordAuthenticationFilter. Concretely, call
addFilterBefore(rateLimitFilter, LoginLockoutFilter.class) and then
addFilterBefore(loginLockoutFilter, UsernamePasswordAuthenticationFilter.class)
(using the actual filter implementation classes for LoginLockoutFilter) in the
SecurityConfig configuration where the filters are registered.
In
`@src/main/java/org/example/projektarendehantering/infrastructure/security/LoginLockoutFilter.java`:
- Around line 47-48: The redirect currently URL-encodes a numeric retryAfter
value unnecessarily; in LoginLockoutFilter replace the URLEncoder.encode usage
and append decision.retryAfterSeconds() directly (or its String form) when
building the "/login?error=true&locked=true&retryAfter=" redirect, and make the
same change in LoginAuthenticationFailureHandler at the analogous location (line
~38) so the numeric seconds are inlined without encoding.
In `@src/main/resources/static/app.js`:
- Around line 110-133: Thottle scroll handler by coalescing
syncActiveSlideFromViewport calls with requestAnimationFrame: add a module-level
flag (e.g., isTicking) and in the scroll listener (the
window.addEventListener("scroll", ...)) only schedule
syncActiveSlideFromViewport via requestAnimationFrame when not already
scheduled, clearing the flag inside syncActiveSlideFromViewport after work
completes; ensure the listener remains passive and keep references to
slides/getBoundingClientRect logic as-is so goToSlide-triggered programmatic
scrolls are coalesced and avoid repeated forced layouts.
- Around line 302-310: Replace the two goToSlide calls and the if/else block
with a single call: determine hash and initialIndex as before, set
activeSlideIndex, then call goToSlide(activeSlideIndex, { behavior: "auto",
shouldScroll: !!hash, updateHash: false }); and remove the redundant first call
and the separate conditional branch (including the alternate updateProgress()
call) since goToSlide runs syncBySlide() and will handle progress/scrolling
appropriately.
In `@src/main/resources/templates/login/login.html`:
- Around line 24-26: Update the login template to display the lockout remaining
time provided by LoginLockoutFilter by reading the retryAfter request param
instead of the static phrase; inside the existing th:if="${param.locked}" block
use param.retryAfter (e.g. show "${param.retryAfter} seconds" when present,
fallback to the current "a few minutes" text) so users see the actual wait time
returned by LoginLockoutFilter.
In
`@src/test/java/org/example/projektarendehantering/infrastructure/security/LoginAttemptServiceTest.java`:
- Around line 13-51: Add tests to verify whether LoginAttemptService keys by
(username, ip) pair or by single dimension: create new test(s) that use
LoginAttemptService (constructor used in existing tests) and call recordFailure
for "user@example.com" from "127.0.0.1" enough times to hit the threshold, then
assert currentLockDecision("user@example.com","127.0.0.2").locked() is false
(different IP) and/or assert
currentLockDecision("other@example.com","127.0.0.1").locked() is false
(different user); also add the inverse case if needed to document behavior.
Target the same class and methods: LoginAttemptService.recordFailure,
LoginAttemptService.currentLockDecision, and LoginAttemptService.recordSuccess
to show expected per-IP vs per-user semantics.
In
`@src/test/java/org/example/projektarendehantering/infrastructure/security/LoginAuthenticationHandlersTest.java`:
- Around line 32-47: Add a new test that verifies normalization consistency
between the submitted username and the authentication principal: create a
LoginAuthenticationSuccessHandler with a mocked LoginAttemptService, simulate a
request whose form username parameter is a non-canonical value (e.g., "
User@Example.com "), set request remoteAddr, build an Authentication whose
getName() returns the canonical "user@example.com", call
handler.onAuthenticationSuccess(request, response, authentication), and verify
LoginAttemptService.recordSuccess was called with the canonical username
("user@example.com") and the request IP; ensure the response redirect assertion
("/home") remains.
In
`@src/test/java/org/example/projektarendehantering/infrastructure/security/LoginLockoutFilterTest.java`:
- Around line 12-50: Add tests to LoginLockoutFilterTest that exercise
non-matching requests: create cases invoking LoginLockoutFilter.doFilter with
(1) a GET "/login" request (MockHttpServletRequest with method "GET") and assert
the filter does not call redirect and the chain proceeded, (2) a POST to a
different path like "/register" or "/api/foo" and assert the same, and (3) a
POST "/login" with no "username" parameter to verify no NPE and that the request
is passed through; in each test mock LoginAttemptService.currentLockDecision
only if expected to be called (and verify it is not called for non-matching
requests) and use MockFilterChain/MockHttpServletResponse assertions
(response.getRedirectedUrl() == null, response.getStatus()==200 or chain
proceeded) to confirm correct behavior.
In
`@src/test/java/org/example/projektarendehantering/infrastructure/security/RateLimitServiceTest.java`:
- Around line 58-79: Extract the private MutableClock inner class used in
RateLimitServiceTest into a shared package-private test utility so
LoginAttemptServiceTest can reuse it; specifically, create a new test helper
class named MutableClock (matching the current constructor and overrides:
instant(), getZone(), withZone()) in a shared test package (e.g.,
infrastructure.security.testing) and replace the private inner MutableClock in
both RateLimitServiceTest and LoginAttemptServiceTest with imports of that
shared MutableClock. Ensure the new helper preserves the same API (constructor
taking Instant and the three overridden methods) and visibility so both tests
can access it.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 2daa6848-b16c-4256-bb6d-01fb5b1edf69
📒 Files selected for processing (20)
pom.xmlsrc/main/java/org/example/projektarendehantering/infrastructure/config/SecurityConfig.javasrc/main/java/org/example/projektarendehantering/infrastructure/security/LoginAttemptService.javasrc/main/java/org/example/projektarendehantering/infrastructure/security/LoginAuthenticationFailureHandler.javasrc/main/java/org/example/projektarendehantering/infrastructure/security/LoginAuthenticationSuccessHandler.javasrc/main/java/org/example/projektarendehantering/infrastructure/security/LoginLockoutFilter.javasrc/main/java/org/example/projektarendehantering/infrastructure/security/RateLimitFilter.javasrc/main/java/org/example/projektarendehantering/infrastructure/security/RateLimitService.javasrc/main/java/org/example/projektarendehantering/infrastructure/security/SecurityObservabilityService.javasrc/main/resources/application.propertiessrc/main/resources/static/app.csssrc/main/resources/static/app.jssrc/main/resources/templates/landing.htmlsrc/main/resources/templates/login/login.htmlsrc/test/java/org/example/projektarendehantering/infrastructure/security/LoginAttemptServiceTest.javasrc/test/java/org/example/projektarendehantering/infrastructure/security/LoginAuthenticationHandlersTest.javasrc/test/java/org/example/projektarendehantering/infrastructure/security/LoginLockoutFilterTest.javasrc/test/java/org/example/projektarendehantering/infrastructure/security/RateLimitFilterTest.javasrc/test/java/org/example/projektarendehantering/infrastructure/security/RateLimitServiceTest.javasrc/test/java/org/example/projektarendehantering/infrastructure/security/SecurityObservabilityServiceTest.java
Closes #89
…swell
Summary by CodeRabbit
Release Notes
New Features
Style
Chores